﻿/* ============================================================================
(c) Copyright 2012 Hewlett-Packard Development Company, L.P.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge,publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
============================================================================ */
using System;
using HPCloud.Objects.Domain;
using HPCloud.Common.Properties;
using HPCloud.Objects.Utility;
using System.Collections.Specialized;
using System.Net.Http;
using System.Diagnostics.Contracts;
using HPCloud.Common;
using System.Collections.Generic;
using System.Linq;
using System.IO;

namespace HPCloud.Objects.DataAccess.OpenStack
{
    public class ObjectRepository : BaseRepository, IObjectRepository
    {
        const string cCopySource = "X-Copy-From";
        const string cDelimiter = "/";
        Settings _settings;

        #region Ctors
//======================================================================================================================
/// <summary>
/// 
/// </summary>
/// <param name="settings"></param>
//======================================================================================================================
        public ObjectRepository(RepositoryContext context) : base(context)
        {}
        #endregion
        #region Folder Operations
//======================================================================================================================
/// <summary>
/// 
/// </summary>
/// <param name="path"></param>
//======================================================================================================================
        public void MakeFolder(StoragePath targetPath)
        {
            StorageObject storageObject = new StorageObject();

            if (targetPath.ResourcePath.EndsWith("/") == false)
            {
                storageObject.Key = targetPath.ResourcePath + @"/";
            }
            else
            {
                storageObject.Key = targetPath.ResourcePath;
            }

            storageObject.Load("");
            storageObject.StorageObjectType = StorageObjectType.Folder;
            HttpContent content = storageObject.ToHttpContent();
            content.Headers.Remove("Content-Type");
            content.Headers.Add("Content-Type", "application/directory");
            this.InvokeEndpoint(HttpMethod.Put, targetPath.AbsoluteURI, null, content, null);
        }
//======================================================================================================================
/// <summary>
/// 
/// </summary>
/// <param name="path"></param>
//======================================================================================================================
        public void MakeFolder(string path)
        {
            if (path.EndsWith("/"))
            {
                path = path.TrimEnd();
            }

            if (path.StartsWith(@"\") || path.StartsWith("/"))
            {
                path = path.Substring(0, path.Length - 1);
            }

            path                   = path.Replace(@"\", "/");
            StoragePath targetPath = new StoragePath(path);
            this.MakeFolder(targetPath);
        }
//======================================================================================================================
/// <summary>
/// 
/// </summary>
/// <param name="sourcePath"></param>
//======================================================================================================================
        public void DeleteFolder(StoragePath targetPath)
        {
            // Use recursion to grab a flat list of folder and file storageObjects from the supplied location.

            StorageObjects storageObjects = this.GetStorageObjects(targetPath, true);

            // Destroy everything we find.  

            if (storageObjects != null)
            {
                foreach (StorageObject storageObject in storageObjects.List)
                {
                    this.DeleteStorageObject(new StoragePath(this.Context.ServiceDescription.Url + cDelimiter + targetPath.Volume + cDelimiter + storageObject.Key));
                }
            }

            // Finally kill the target marker..

            this.DeleteStorageObject(targetPath);
        }
////======================================================================================================================
///// <summary>
///// 
///// </summary>
///// <param name="sourcePath"></param>
///// <param name="targetPath"></param>
///// <param name="preserveMetaDataOnCopy"></param>
///// <param name="recurse"></param>
////======================================================================================================================
        public void CopyFolder(StoragePath sourcePath, StoragePath targetPath, bool recurse = true)
        {
            Contract.Requires(sourcePath.IsFolderPathOnly == true);
            Contract.Requires(targetPath.IsFolderPathOnly == true);

            this.CopyFolder(sourcePath, targetPath, false, recurse);
        }
////======================================================================================================================
///// <summary>
///// 
///// </summary>
///// <param name="sourcePath"></param>
///// <param name="targetPath"></param>
///// <param name="useLocalFileMetaData"></param>
////======================================================================================================================
        public void CopyFolder(StoragePath sourcePath, StoragePath targetPath, bool preserveMetaDataOnCopy, bool recurse = true, Action afterCopyAction = null)
        {
            Contract.Requires(sourcePath.IsFolderPathOnly == true);
            Contract.Requires(targetPath.IsFolderPathOnly == true);

            if (sourcePath.PathType == PathType.Local && targetPath.PathType == PathType.Remote)
            {
                this.CopyLocalToServer_Folder(sourcePath, targetPath, preserveMetaDataOnCopy, recurse, afterCopyAction);
            }
        }
////======================================================================================================================
///// <summary>
///// 
///// </summary>
///// <param name="sourcePath"></param>
///// <param name="targetPath"></param>
///// <param name="preserveMetaDataOnCopy"></param>
///// <param name="preserveACLDataOnCopy"></param>
///// <param name="useLocalFileMetaData"></param>
////======================================================================================================================
        private void CopyLocalToServer_Folder(StoragePath sourcePath, StoragePath targetPath, bool preserveMetaDataOnCopy, bool recurse, Action afterCopyAction)
        {
            List<string> fileNames = System.IO.Directory.GetFiles(sourcePath.Path).ToList();

            foreach (string filename in fileNames)
            {
                StorageObject storageObject = new StorageObject();
                storageObject.LoadFile(sourcePath.Path + filename);
                storageObject.Key = targetPath.Path;

                this.SaveStorageObject(storageObject);
            }
        }
        #endregion
        #region Object Copy Operations
////======================================================================================================
/// <summary>
/// 
/// </summary>
/// <param name="source"></param>
/// <param name="target"></param>
/// <param name="recursive"></param>
////======================================================================================================
        public void Copy(string source, string target, bool recursive = false)
        {
            StoragePath sourcePath = new StoragePath(source);
            StoragePath targetPath = new StoragePath(target);

            // Make sure that the User is not trying to perform a local copy only. this is an error as we don't duplicate cmd.exe functionality in this provider..

            if (sourcePath.PathType == PathType.Local && targetPath.PathType == PathType.Local)
            {
                throw new InvalidOperationException("Local file copy operations are not supported.");
            }
            else if (sourcePath.PathType == PathType.Local && targetPath.PathType == PathType.Remote)
            {
                this.CopyLocalToServer(sourcePath, targetPath);
            }
            else if (sourcePath.PathType == PathType.Remote && targetPath.PathType == PathType.Local)
            {
                this.CopyServerToLocal(sourcePath, targetPath);
            }
            else if (sourcePath.PathType == PathType.Remote && targetPath.PathType == PathType.Remote)
            {
                this.CopyIntraServer(sourcePath, targetPath);
            }
        }
////======================================================================================================
///// <summary>
///// 
///// </summary>
///// <param name="sourcePath"></param>
///// <param name="targetPath"></param>
////======================================================================================================
        public void CopyIntraServer(StoragePath sourcePath, StoragePath targetPath)
        {
            Contract.Requires(targetPath.IsFolderPathOnly == true, "You must supply a valid folder for the target location argument.");

            // Make sure that the User isn't trying to copy a folder onto a file or something weird like that..
            // If not, and they want to do an IntraServer copy from one folder to another, handle it now..
            // Here's the situation, the IntraServer Copy is file based so deal with it..

            NameValueCollection headers = new NameValueCollection();
            headers.Add(cCopySource, "/" + sourcePath.ResourcePath);
            this.InvokeEndpoint(HttpMethod.Put, targetPath.AbsoluteURI, null, headers);
        }
////======================================================================================================================
///// <summary>
///// 
///// </summary>
////======================================================================================================================
        public void CopyLocalToServer(string targetPath, StorageObject storageObject)
        {
            storageObject.Key = targetPath;
            this.SaveStorageObject(storageObject);
        }
 //======================================================================================================================
///// <summary>
///// 
///// </summary>
///// <param name="sourcePath"></param>
///// <param name="targetPath"></param>
///// <param name="recurse"></param>
//======================================================================================================================
        public void CopyServerToLocal(StoragePath sourcePath, StoragePath targetPath)
        {
            Contract.Requires(sourcePath.IsFolderPathOnly == false);
            Contract.Requires(targetPath.IsFolderPathOnly == true);

            // Verify the sourcePath is a valid argument..

            StorageObject storageObject = this.GetStorageObject(sourcePath);
            FileStream stream           = new FileStream(targetPath.ResourcePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
            
            storageObject.Save(stream);
        }
////======================================================================================================
///// <summary>
///// 
///// </summary>
///// <param name="targetPath"></param>
///// <param name="storageObject"></param>
////======================================================================================================
        public void CopyLocalToServer(Domain.StoragePath targetPath, Domain.StorageObject storageObject)
        {
            this.InvokeEndpoint(HttpMethod.Put, targetPath.AbsoluteURI, null, storageObject.ToHttpContent(),null);
        }
        #endregion
        #region Objects - Delete Operations
////======================================================================================================
///// <summary>
///// 
///// </summary>
///// <param name="targetPath"></param>
///// <param name="permanentDelete"></param>
////======================================================================================================
        public void DeleteStorageObject(Domain.StoragePath targetPath)
        {
            this.InvokeEndpoint(HttpMethod.Delete, targetPath.AbsoluteURI, null, null);
        }
        #endregion
        #region Objects - Retrieval and Persistence Operations
////======================================================================================================================
///// <summary>
///// 
///// </summary>
///// <param name="path"></param>
///// <returns></returns>
////======================================================================================================================
        public void SetMetaData(StoragePath sourcePath, StorageObject storageObject)
        {
            Contract.Requires(sourcePath != null, "You must supply a valid StoragePath to complete this operation.");
            Contract.Requires(storageObject != null, "You must supply a non-null storageObject with some Extended Properties");
            Contract.Requires(storageObject.ExtendedProperties.Count > 0, "You have not supplied any MetaData to update within the StorageObjects Extended Properties collection.");

            using (HttpClient client = this.CreateHttpClient())
            {
                foreach (KeyValuePair<string, string> element in storageObject.ExtendedProperties)
                {
                    client.DefaultRequestHeaders.Add("X-Object-Meta-" + element.Key, element.Value);
                }

                StorageObject so = new StorageObject();
                so.Load(".");
                HttpResponseMessage msg = client.Post(sourcePath.ToUri(), so.ToHttpContent());
            }
        }
////======================================================================================================================
///// <summary>
///// 
///// </summary>
///// <param name="path"></param>
///// <returns></returns>
////======================================================================================================================
        public List<KeyValuePair<string, string>> GetMetaData(StoragePath sourcePath)
        {
            Contract.Requires(sourcePath != null, "You must supply a valid StoragePath to complete this operation.");

            using (HttpClient client = this.CreateHttpClient())
            {
                client.MaxResponseContentBufferSize = 7196608;
                HttpRequestMessage request = this.CreateRequestMessage(HttpMethod.Head, sourcePath.ToUri(), null);
                HttpResponseMessage msg    = client.Send(request);

                if (this.IsStatusNormal(msg.StatusCode))
                {
                    msg.Content.LoadIntoBuffer();
                    List<KeyValuePair<string, string>> extendedProperties = new List<KeyValuePair<string, string>>();

                    foreach (KeyValuePair<string, IEnumerable<string>> header in msg.Headers)
                    {
                        if (header.Key.StartsWith("X-Object-Meta"))
                        { 
                            extendedProperties.Add(new KeyValuePair<string,string>(header.Key.Replace("X-Object-Meta-", string.Empty), header.Value.First()));
                        }
                    }

                    return extendedProperties;
                }
                else
                {
                    //throw ExceptionFactory.CreateStorageObjectRepositoryException(msg);
                }
            }
            
            return null;
        }
////======================================================================================================
///// <summary>
///// 
///// </summary>
///// <param name="path"></param>
///// <returns></returns>
////======================================================================================================
        public Domain.StorageObject GetStorageObject(string path)
        {
            StoragePath storagePath = new StoragePath(this.Context.ServiceDescription.Url + cDelimiter + path);            
            return this.GetStorageObject(storagePath);
        }
////======================================================================================================
///// <summary>
///// 
///// </summary>
///// <param name="sourcePath"></param>
///// <returns></returns>
////======================================================================================================
        public Domain.StorageObject GetStorageObject(Domain.StoragePath sourcePath)
        {
            using (HttpClient client = this.CreateHttpClient())
            {               
                HttpResponseMessage msg = client.Get(sourcePath.ToUri());

                if (this.IsStatusNormal(msg.StatusCode))
                {
                    msg.Content.LoadIntoBuffer();
                    StorageObject storageObject = new StorageObject();
                    storageObject.Key           = sourcePath.ResourcePath;

                    storageObject.Load(msg.Content.ContentReadStream);

                    return storageObject;
                }
                else
                {
                    return null;
                   // throw ExceptionFactory.CreateStorageObjectRepositoryException(msg);
                }
            }
        }
////======================================================================================================================
///// <summary>
///// 
///// </summary>
///// <returns></returns>
////======================================================================================================================
        static StorageObjects GetNodeContents(StoragePath sourcePath, string storageManagementUrl, string authToken)
        {
            Contract.Requires(sourcePath.IsFolderPathOnly == true, "You must pass a path to a folder to complete this operation.");

            NameValueCollection queryString = new NameValueCollection();

            if (sourcePath.Path != "")
                queryString.Add("path", sourcePath.Path);
            else
                queryString.Add("path", "");

            StorageObjects storageObjects = BaseRepository.ListObjects<StorageObjects>(HttpMethod.Get, storageManagementUrl + cDelimiter + sourcePath.Volume, queryString, null, authToken);
            return storageObjects;
        }
////======================================================================================================================
///// <summary>
///// 
///// </summary>
///// <param name="sourcePath"></param>
///// <returns></returns>
////======================================================================================================================
        static bool GetStorageObjectsHierachy(StoragePath sourcePath, ref StorageObjects storageObjects, string storageManagementUrl, string authToken)
        {
            Contract.Requires(sourcePath.IsFolderPathOnly);
            StorageObjects nextNodeContents = GetNodeContents(sourcePath, storageManagementUrl, authToken);

            if (!nextNodeContents.List.Any())
            {
                return false;
            }
            else
            {
                storageObjects.List.AddRange(nextNodeContents.List);
                List<StorageObject> folders = nextNodeContents.GetFolders();

                if (folders != null)
                {
                    foreach (StorageObject folder in folders)
                    {
                        if (GetStorageObjectsHierachy(new StoragePath(storageManagementUrl + "/" + sourcePath.Volume + "/" + folder.Key), ref storageObjects, storageManagementUrl, authToken) == false)
                        {
                            //return false;
                        }
                        else
                        {
                            //return true;
                        }
                    }
                }
            }
            return false;
        }
////======================================================================================================
///// <summary>
///// 
///// </summary>
///// <param name="sourcePath"></param>
///// <param name="recurse"></param>
///// <returns></returns>
////======================================================================================================
        public HPCloud.Objects.Domain.StorageObjects GetStorageObjects(StoragePath sourcePath, bool recurse)
        {
            Contract.Requires(sourcePath.IsFolderPathOnly == true, "You must pass a path to a folder to complete this operation.");
            Contract.Requires(sourcePath.PathType == PathType.Remote, "You must pass a path to a remote folder to complete this operation.");

            NameValueCollection queryString = new NameValueCollection();
            if (sourcePath.Path != "" && sourcePath.Path != "/")
            {
                if (sourcePath.Path.StartsWith("/"))
                    queryString.Add("path", sourcePath.Path.TrimStart('/'));
                else
                    queryString.Add("path", sourcePath.Path);
            }
            else
                queryString.Add("path", "");

            if (recurse)
            {
                StorageObjects hierarchy = new StorageObjects();
                GetStorageObjectsHierachy(sourcePath, ref hierarchy, this.Context.ServiceDescription.Url, this.Context.AccessToken.Id);
                return hierarchy;
            }
            else
            {
                StorageObjects storageObjects = this.InvokeEndpoint<StorageObjects>(HttpMethod.Get, this.Context.ServiceDescription.Url + cDelimiter + sourcePath.Volume, queryString, null);
                return storageObjects;
            }
        }
////======================================================================================================
///// <summary>
///// 
///// </summary>
///// <param name="storageContainer"></param>
///// <param name="path"></param>
///// <param name="recurse"></param>
///// <returns></returns>
////======================================================================================================
        public StorageObjects GetStorageObjects(string volume, string path, bool recurse = false)
        {
            StoragePath storagePath         = null;
            NameValueCollection queryString = new NameValueCollection();

            if (path == null)
            {
                storagePath = new StoragePath(this.Context.ServiceDescription.Url + cDelimiter + volume);               
            }
            else
            {
                storagePath = new StoragePath(this.Context.ServiceDescription.Url + cDelimiter + volume + cDelimiter + path);
               
            }

            if (recurse)
            {
                StorageObjects hierarchy = new StorageObjects();
                GetStorageObjectsHierachy(storagePath, ref hierarchy, this.Context.ServiceDescription.Url, this.Context.AccessToken.Id);
                return hierarchy;
            }
            else
            {
                StorageObjects storageObjects = this.InvokeEndpoint<StorageObjects>(HttpMethod.Get, this.Context.ServiceDescription.Url + cDelimiter + volume, null, null);

                if (storageObjects.List.Count > 0)
                {
                    foreach (StorageObject storageObject in storageObjects.List)
                    {
                        if (storageObject.StorageObjectType == StorageObjectType.Folder)
                        {
                            storageObject.ObjectType = "Folder";
                        }
                    }
                }
                return storageObjects;
            }
        }
//////======================================================================================================================
/////// <summary>
/////// 
/////// </summary>
/////// <param name="storageObject"></param>
//////======================================================================================================================
        public void SaveStorageObject(StorageObject storageObject)
        {
                        

            string resourcePath = this.Context.ServiceDescription.Url + "/" + storageObject.Key.Replace(@"\", @"/");
            this.InvokeEndpoint(HttpMethod.Put, resourcePath, null, storageObject.ToHttpContent(), null);
        }
        #endregion
    }
}
